Skip to content

Conversation

@prithayan
Copy link
Contributor

@prithayan prithayan commented Nov 11, 2025

This PR enhances the existing --hw-flatten-modules pass with configurable heuristics, hierarchical path support, and module NLA protection for fine-grained control over module inlining.

The FlattenModules pass now supports:

  • Configurable inlining heuristics (empty, no-outputs, single-use, small modules)
  • HierPath updating when inlining instances with inner symbols
  • Inner symbol renaming using InnerSymbolNamespace to avoid conflicts

Inlining Heuristics (7 new options)

--inline-all             (default: true)  - Inline all private modules (original behavior)
--inline-empty           (default: true)  - Inline empty modules
--inline-no-outputs      (default: true)  - Inline modules with no outputs
--inline-single-use      (default: true)  - Inline single-use modules
--inline-small           (default: true)  - Inline small modules
--small-threshold        (default: 8)     - Size threshold for small modules
--inline-with-state      (default: false) - Allow inlining stateful modules

This PR also adds new lit tests for testing these enhancements.

@prithayan prithayan requested a review from darthscsi as a code owner November 11, 2025 04:34
@prithayan prithayan marked this pull request as draft November 11, 2025 04:46
@prithayan prithayan changed the title [HW] Add HWInliner pass for inlining private HW modules [HW] Enhance FlattenModules pass with configurable inlining heuristics Nov 11, 2025
@prithayan prithayan force-pushed the dev/pbarua/hwinliner branch from 2d14fe9 to f3e4ccf Compare November 11, 2025 16:57
@prithayan prithayan force-pushed the dev/pbarua/hwinliner branch from f3e4ccf to c921ca7 Compare November 11, 2025 17:12
@prithayan prithayan marked this pull request as ready for review November 12, 2025 18:19
Copy link
Member

@seldridge seldridge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

I didn't review the inner sym / hierarchical path update logic in much detail, though. I realize this was probably the most important thing to review. However, the tests are doing the right thing!

Comment on lines +64 to +65
Option<"smallThreshold", "small-threshold", "unsigned", "8",
"Maximum number of operations for a module to be considered small">,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Gating this behind --inline-small-threshold would be more obvious to me as a user.

"Allow inlining of modules that contain state (seq.firreg operations)">,
Option<"inlineAll", "inline-all", "bool", "true",
"Inline all private modules regardless of heuristics (default behavior)">
];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Maybe prefixing these with --hw would be good. I've grown to liking having any option that is only for specific dialects is useful to indicate which dialect it works on. E.g., --dedup-classes in firtool would be better as --firrtl-dedup-classes. The same thing holds here, too.

Comment on lines +71 to +79
InnerSymbolNamespace *ns, HWModuleOp parentModule,
HWModuleOp sourceModule,
DenseMap<StringAttr, StringAttr> *symMapping,
mlir::AttrTypeReplacer *replacer, HierPathTable *pathsTable,
hw::InnerRefAttr instanceRef)
: InlinerInterface(context), prefix(prefix), ns(ns),
parentModule(parentModule), sourceModule(sourceModule),
symMapping(symMapping), replacer(replacer), pathsTable(pathsTable),
instanceRef(instanceRef) {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This constructor may be unnecessary if you use struct initialization. E.g., PrefixingInliner{a, b, c}. I don't know if there's any utility in adding a one-to-one constructor here as opposed to the {} one.

bool isEmpty = bodySize == 1;
bool hasNoOutputs = module.getNumOutputPorts() == 0;
bool hasOneUse = instanceNode->getNumUses() == 1;
bool hasState = !module.getOps<seq::FirRegOp>().empty();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about memories or other kinds of state?

Comment on lines +11 to +17
hw.module @TestSingleUse(in %x: i4, out y: i4) {
// NONE-NEXT: hw.instance "small" @SmallModule
// SINGLE-NEXT: %[[V0:.+]] = comb.add %x, %x
// SINGLE-NEXT: hw.output %[[V0]]
%0 = hw.instance "small" @SmallModule(a: %x: i4) -> (b: i4)
hw.output %0 : i4
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this may want to use a @LargeModule to definitely avoid this getting inlined with the small option. I.e., there could be a bug where small inlining is turned on and it would be good if this was test was not susceptible to that.

Comment on lines +32 to +35
hw.module private @TinyModule(in %a: i4, out b: i4) {
%0 = comb.add %a, %a : i4
hw.output %0 : i4
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This is just @SmallModule. Though, going off of the comment above, I think you want small to be "large" and tiny to be "small".

Comment on lines +31 to +42
// Test that public modules are not inlined
// CHECK-LABEL: hw.module @DontInlinePublic
hw.module @DontInlinePublic(in %x: i4, out y: i4) {
// CHECK-NEXT: hw.instance "pub" @PublicModule
%0 = hw.instance "pub" @PublicModule(a: %x: i4) -> (b: i4)
hw.output %0 : i4
}
// CHECK: hw.module @PublicModule
hw.module @PublicModule(in %a: i4, out b: i4) {
%0 = comb.add %a, %a : i4
hw.output %0 : i4
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aside to think about: we could inline these still, just we can't delete them.

// CHECK-NOT: hw.instance
// CHECK-NEXT: %[[V0:.+]] = comb.add %x, %x
// CHECK-NEXT: %[[W0:.+]] = hw.wire %[[V0]] sym @[[SYM:wire_op]]
// CHECK-NEXT: sv.verbatim "ref to {{[{][{]}}0{{[}][}]}}" {symbols = [#hw.innerNameRef<@InlineWithInnerRef::@[[SYM]]>]}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mega-nit: There is only this one verbatim and the test could avoid the {{{{{}}}}}} shenanigans with just:

CHECK-NEXT: sv.verbatim {{.*}} {symbols = ...}

Or:

CHECK-NEXT: sv.verbatim
CHECK-SAME:   {symbols = [#hw.innerNameRef<@InlineWithInnerRef::@[[SYM]]>]}

Ditto throughout.

hw.module @InlineWithInnerRef(in %x: i4, out y: i4) {
// CHECK-NOT: hw.instance
// CHECK-NEXT: %[[V0:.+]] = comb.add %x, %x
// CHECK-NEXT: %[[W0:.+]] = hw.wire %[[V0]] sym @[[SYM:wire_op]]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: there isn't any renaming here, so capturing this into SYM doesn't serve a purpose. The test probably does want to not capture wire_op exactly and should be .+ or an alpha-numeric capture. It's probably best to just use @wire_op directly if the expectation is this that this will be preserved exactly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants